Assembly Básico

Introdução

Esse artigo é introdutório para a linguagem Assembly, ele não tem interesse em aprofundar-se. Além do básico, também terá as principais ferramentas para manipular essa linguagem.

O que é Assembly

Para entender o que é essa linguagem de programação, é primeiro necessário entendermos um pouco do funcionamento dos computadores, especificamente a parte considerada o "cérebro" do computador, a CPU.

Central Processing Unit - CPU

Também conhecida como processador, é a parte responsável por fazer diversas operações lógicas, aritméticas, processamento de dados e executar o código de máquina de um programa de computador (que é a parte que mais nos interessa).

O código de máquina é um conjunto de instruções definidas por sua arquitetura, denominada como ISA (Instruction Set Archtecture), e elas são comumente representadas em hexadecimal como mostra o exemplo:


31 c0
b8 28 23 00 00
50
bb 10 90 12 76
ff d3
		

Montagem ou Assembly

Do ponto de vista humano, entender instruções em código de máquina é muito difícil. Por isso os manuais da ISA, para facilitar a criação, simplificam o entendimento das instruções se referindo à elas em formato de texto. Esse formato é conhecido como notação mnemônico.


31 c0              XOR EAX, EAX  
b8 28 23 00 00     MOV EAX, 0X2328
50                 PUSH EAX
bb 10 90 12 76     MOV EBX, 0X76129010
ff d3              CALL EBX
		

Sendo essa notação usada para montar o código de máquina, o que a gente conhece atualmente como "linguagem Assembly" (Assembly remete a assembler que se traduz em montagem). Na verdade não existe uma única linguagem mas sim cada ISA tem uma linguagem Assembly diferente.

Anteriormente, os programadores escreviam seus códigos usando a notação em texto e depois manualmente convertiam para código de máquina. Porém, atualmente existem softwares que fazem esse processo automaticamente, chamados de assemblers.

Diferentes Arquiteturas

Existem algumas arquiteturas diferentes, e entre elas existem diferenças significativas. As arquiteturas mais conhecidas são a x86 e x86-64. Dentre as diferenças existentes destaca-se as seguintes:

Tamanho da capacidade

As arquiteturas se diferem no tamanho da capacidade padrão de dados. Como exemplo, x86 é compatível com 32 bits e o x86-64 com 64 bits. E ainda, sistemas 64 bits permitem operar com diferentes tamanhos de dados e acessar quantidades maiores de memória.

Registros de propósito geral

O número de registradores (pequenas unidades de armazenamento rápido) variam. Sendo que, arquiteturas x86 têm menos registradores em comparação com x86-64.

Stack

A stack ou pilha em português, é um espaço continuo de memória que os programas usam para manipular dados. E ainda, é utilizada para a "comunicação" durante chamadas e retornos de funções.

A stack ainda tem o comportamento LIFO (Last In First Out, em português Ultimo que entra é o primeiro a sair), como o nome diz replicando a característica de uma pilha. Logo, vale citar que quando inserindo informações na stack é necessário levar em consideração essa condição e inverter a ordem das informações.

E existe ainda alguns endereços fundamentais como:

Sintaxe Básica

Sintaxe Intel x AT&T

Existem duas sintaxes diferentes para programar em Assembly (Intel ou AT&T).

A sintaxe da Intel é no seguinte formato: MOV EAX, 3

Já a sintaxe AT&T: MOV $0X3, %EAX

O artigo será escrito usando a sintaxe Intel.

Instruções

InstruçãoFunção
MOVMove
ADDAdiciona
SUBSubtrai
INCIncrementa
DECDescrementa
CALLChama
JMPSalta
JNESalta se não for igual
CMPCompara
PUSHColoca na Stack (Topo)
POPRetira da Stack (Topo)
NOPNo Operation (\x90)
INT3Interrupção (Breakpoint)
XORInstruções Lógicas

Exemplos de uso


MOV EAX, 3 # Coloca o valor 3 no endereço de memória EAX
MOV EAX, [ESP] # Coloca o conteúdo do topo da Stack para EAX
XOR EAX, EAX # Zera o valor em EAX
NOP # Não faz nada
PUSH 0x3 # Envia o valor 3 em Hex para o topo da Stack
POP EAX # Coloca o valor do topo da Stack para EAX
	

Compilação

Para transformar o código em Assembly para um arquivo executável é necessário fazer dois procedimentos, sendo eles o assembler e "linkagem"

Desenvolvendo para Windows

Quando desenvolvendo em Assembly para Windows é necessário seguir alguns processos. Primeiramente, é fundamental ter em mãos o site da Microsoft onde terá todas as funções que serão referenciadas e os arquivos que devem ser linkados https://learn.microsoft.com/en-us/windows/win32/api/

Como exemplo temos o seguinte código usado para criar uma caixa exibindo uma mensagem:


extern _MessageBoxA ; declarando a funcao externa que sera usada

global main

section .data ; espaco dedicado a declarar variaveis
    msg db "Mensagem teste",0 ; db stands for declarative byte
    titulo db "Titulo da caixa",0 ; ,0 serve como uma quebra de linha na secao de declaracao de variaveis
		    
section .text
main:
    PUSH 0
    PUSH titulo
    PUSH msg
    PUSH 0
    CALL _MessageBoxA
		

Onde temos a parte superior onde definimos que será usada uma função externa _MessageBoxA usando o comando extern, e logo após temos a definições das variáveis que serão usadas.

E acompanhando a sintaxe provida pelo site temos que está em C++:

int MessageBox( 
    [in, optional] HWND hWnd, # Um valor handle que recebe o valor 0
    [in, optional] LPCTSTR lpText, # O valor que estará no texto
    [in, optional] LPCTSTR lpCaption, # O título da caixa
    [in] UINT uType # O tipo da caixa (Olhar o site para ver cada exemplo)
);
		

E considerando que na stack todos os comandos terão que ser enviados ao contrário temos a ordem:


main:
	PUSH 0 # Valor do tipo
	PUSH titulo # Texto do título
	PUSH msg # Texto da caixa
	PUSH 0 # Valor do handle
	CALL _MessageBoxA
		

Por fim chamando o espaço da memória onde está a função da caixa de mensagem.

Compilando no Windows

Quando estamos compilando o código no Windows usaremos os nasm e o golink da seguinte forma: nasm -f win32 projeto.asmgolink /entry bloco principal de codigo projeto.obj arquivo a ser linkado ao código

Seguindo o exemplo mostrado acima usaremos os seguintes comandos e seguindo as necessidades do site da Microsoft:


nasm -f win32 caixa.asm
golink /entry main caixa.obj User32.dll
		

Assim teremos o arquivo .exe funcional.

Desenvolvendo para Linux

Para desenvolver em linux o processo é bem diferente se comparado com o Windows. Primeiramente é fundamental conhecer a ferramenta man, que é um manual digital onde podemos ver sobre as funções e ferramentas do sistema. Essa ferramenta é fundamental pois através dela teremos acesso ao manual ISA do nosso sistema.

E no linux nos usaremos as chamadas syscall para trazer as funcionalidades ao nosso código. Então para começar devemos fazer o seguinte comando: man syscall

E apesar de todo o manual ser muito útil, o que mais nos interessa são as tabelas Architecture calling conventions, onde veremos qual é a instrução que fará a chamada da função do sistema, e a tabela usada para passar aos argumentos. Para praticidade irei colocar aqui somente as que nos interessa, porém vale sempre usar o man como referência.

 
Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
───────────────────────────────────────────────────────────────────
i386        int $0x80             eax     eax  edx  -
x86-64      syscall               rax     rax  rdx  -        5
───────────────────────────────────────────────────────────────────

Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
i386          ebx   ecx   edx   esi   edi   ebp   -
x86-64        rdi   rsi   rdx   r10   r8    r9    -
──────────────────────────────────────────────────────────────
		

Primeiro iremos usar a arquitetura x86 ou como está escrito i386. Teremos que abrir o arquivo que tem o código que identifica cada syscall, que fica na pasta asm em algum lugar do endereço /usr/include/, no caso atual usaremos o arquivo unistd_32.h. Usaremos os seguinte código que mostra o famoso "Hello World" como referência:


global main
section .data
    teste: db 'Hello Word', 0xa
section .text
main:
    MOV EAX, 4
    MOV EBX, 1
    MOV ECX, teste
    MOV EDX, 11
    INT 0X80
    
    MOV EAX, 1
    MOV EBX, 0
    INT 0X80
		

Nesse código temos 2 funções sendo usadas, sendo a primeira a função write que, de acordo com o arquivo citado acima, possui o código 4 que foi colocado no registrador EAX conforme citado na tabela. E olhando o manual dessa função por meio do comando man 2 write, vemos que ela recebe 3 argumentos, sendo eles o FD (File Descriptor) que recebe o valor 1 para somente mostrar na tela, o texto que será usado e o tamanho do texto. Logo após temos a função exit usada para fechar o código sem causar nenhum erro.

Agora para a arquitetura x86-64, perceberemos que não existe muitas diferenças. Primeiramente abriremos o arquivo unistd_64.h e perceberemos que as funções possuem números diferentes. E ainda, levando em consideração que a syscall e os registradores são diferentes, teremos o código da seguinte forma:


global main
section .data
    texto: db 'Hello World', 0xa
section .data
main:
    MOV RAX, 1 ; Definindo a função write
    MOV RD1, 1 ; Configurando como STDOUT
    MOV RSI, texto ; Texto
    MOV RDX, 11 ; Tamanho do texto
    SYSCALL ; Chamada para executar a função write

    MOV RAX, 60 ; Definindo a função exit
    MOV RDI, 0 ; Código para sem erros
    SYSCALL ; Chamada da função exit
		

Compilando no Linux

Para compilar o código no linux iremos usar o nasm e o ld da seguinte forma:


nasm -f elf32 arquivo.asm # Para a arquitetura x86
nasm -f elf64 arquivo.asm # Para a arquitetura x86-64
		

ld --entry -m elf_i386 arquivo.o -o arquivo # Para arquitetura x86
ld --entry -m elf_x86_64 arquivo.o -o arquivo # Para arquitetura x86-64
		

Debuggers

Debuggers são ferramentas utilizadas para fazer analises nos softwares após sua compilação. Eles podem ser usado tanto para fazer uma engenharia reversa, quanto para explorar falhas como Buffer Overflow dentre outras funções. Dentre as opções disponíveis recomendo o Immunity Debugger para o Windows, e o gdb para o Linux.

Referências

https://mentebinaria.gitbook.io/assembly

https://eximia.co/entendendo-a-stack-em-sua-forma-mais-primitiva-em-assembly/